/*
 * Copyright (C) 2011 Christopher Probst
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of the 'FoxNet Codec' nor the names of its 
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.foxnet.codec;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @author Christopher Probst
 */
public final class CodecRegistry {

    private static final CodecRegistry INSTANCE = new CodecRegistry();

    public static CodecRegistry getInstance() {
	return INSTANCE;
    }

    private static int getTypeVarIndex(Class<?> clazz, Type neededTypeVar) {
	// Get all type variables
	TypeVariable<?>[] tv = clazz.getTypeParameters();

	// For all available type vars...
	for (int i = 0; i < tv.length; i++) {

	    // Compare the names
	    if (tv[i].getName().equals(neededTypeVar.toString())) {
		return i;
	    }
	}

	// -1 means the type var does not exist!
	return -1;
    }

    private static Class<?> resolveGenericTypeVar(Class<?> problemClass,
	    Type typeVariable, Iterator<Class<?>> remainingClasses) {

	// At first try to find out the index of the undefined type var.
	int typeVarIndex = getTypeVarIndex(problemClass, typeVariable);

	while (remainingClasses.hasNext()) {
	    // Get next class
	    Class<?> clazz = remainingClasses.next();

	    // Get parameterized type!
	    ParameterizedType genSuperclass =
		    (ParameterizedType) clazz.getGenericSuperclass();

	    // Get specific type!
	    typeVariable = genSuperclass.getActualTypeArguments()[typeVarIndex];

	    // Try to convert to class!
	    Class<?> codecType = typeToClass(typeVariable);

	    // Perfectly!
	    if (codecType != null) {
		return codecType;
	    }

	    // Next!
	    typeVarIndex = getTypeVarIndex(clazz, typeVariable);
	}

	throw new IllegalArgumentException(
		"Invalid codec implementation class. Reason: Failed to "
			+ "resolve the generic codec type of " + problemClass
			+ " which is necessary to determine the codec type.");
    }

    private static Class<?> typeToClass(Type typeVariable) {
	// Perfectly!
	if (typeVariable instanceof Class) {
	    return (Class<?>) typeVariable;
	} else if (typeVariable instanceof ParameterizedType) {
	    // Convert
	    ParameterizedType pt = (ParameterizedType) typeVariable;

	    // If raw type instance of class...
	    if (pt.getRawType() instanceof Class) {
		return (Class<?>) pt.getRawType();
	    }
	}
	return null;
    }

    private static Class<?> getGenericCodecType(Deque<Class<?>> classes) {
	if (classes == null) {
	    throw new IllegalArgumentException("classes cannot be null");
	}

	if (classes.isEmpty()) {
	    throw new IllegalArgumentException("classes cannot be empty");
	}

	// Get hte first class of the deque
	Class<?> codecClass = classes.getFirst();

	// Check whether or not the codecClass implements Codec
	if (!Codec.class.isAssignableFrom(codecClass)) {
	    // Wrong class
	    throw new IllegalArgumentException(codecClass
		    + " does not implement " + Codec.class);
	}

	// Used to find the correct type
	Type[] interfaces = codecClass.getGenericInterfaces();

	// If there are no interfaces...
	if (interfaces.length == 0) {
	    // Add superclass to deque
	    classes.addFirst(codecClass.getSuperclass());

	    // Try the superclass...
	    return getGenericCodecType(classes);
	}

	// Check all interfaces
	for (Type interfaze : interfaces) {

	    if (interfaze instanceof ParameterizedType) {
		// Convert
		ParameterizedType paraInterface = (ParameterizedType) interfaze;

		// If Codec.class...
		if (paraInterface.getRawType() == Codec.class) {
		    /*
		     * The first variable has to be available!
		     */
		    Type typeVariable =
			    paraInterface.getActualTypeArguments()[0];

		    // Try to convert to class!
		    Class<?> codecType = typeToClass(typeVariable);

		    // Perfectly!
		    if (codecType != null) {
			return codecType;
		    }

		    /*
		     * This is very important: If the implementation of codec
		     * looks like this "class CodecImpl<T> implements Codec<T>"
		     * we have to ask the subclasses for this generic type var!
		     * Of course, this could end badly, too.
		     */
		    Iterator<Class<?>> it = classes.iterator();
		    return resolveGenericTypeVar(it.next(), typeVariable, it);
		}
	    }
	}

	// Maybe the superclass knows more...
	classes.addFirst(codecClass.getSuperclass());

	// Try superclass...
	return getGenericCodecType(classes);
    }

    // Here we store the codec types linked to their codecs
    private final ConcurrentMap<Class<?>, Class<? extends Codec<?>>> classes =
	    new ConcurrentHashMap<>();

    public CodecBin newCodecBin() throws InstantiationException,
	    IllegalAccessException {
	return new CodecBin(this);
    }

    @SuppressWarnings("unchecked")
    public <T> Class<? extends Codec<T>> bindIfAbsent(
	    Class<? extends Codec<T>> codecClass) {
	if (codecClass == null) {
	    throw new NullPointerException("codecClass cannot be null");
	}

	// Put if absent!
	return (Class<? extends Codec<T>>) classes.putIfAbsent(
		getCodecBindingType(codecClass), codecClass);
    }

    @SuppressWarnings("unchecked")
    public <T> Class<? extends Codec<T>> bind(
	    Class<? extends Codec<T>> codecClass) {
	if (codecClass == null) {
	    throw new NullPointerException("codecClass cannot be null");
	}

	// Replace old codec
	return (Class<? extends Codec<T>>) classes.put(
		getCodecBindingType(codecClass), codecClass);
    }

    public Map<Class<?>, Class<? extends Codec<?>>> getCodecBindings() {
	return Collections
		.unmodifiableMap(new HashMap<Class<?>, Class<? extends Codec<?>>>(
			classes));
    }

    @SuppressWarnings("unchecked")
    public <T> Class<T> getCodecBindingType(Class<? extends Codec<T>> codecClass) {
	if (codecClass == null) {
	    throw new NullPointerException("codecClass cannot be null");
	}

	// Create a new array list which contains the codec class
	Deque<Class<?>> classes = new ArrayDeque<Class<?>>();

	// Add to list
	classes.add(codecClass);

	// Try to get the generic codec type!
	return (Class<T>) getGenericCodecType(classes);
    }

    @SuppressWarnings("unchecked")
    public <T> Class<? extends Codec<T>> getCodecBinding(Class<T> clazz) {
	if (clazz == null) {
	    throw new NullPointerException("clazz cannot be null");
	}

	// Check
	return (Class<? extends Codec<T>>) classes.get(clazz);
    }

    public <T> boolean unbind(Class<? extends Codec<T>> codecClass) {
	if (codecClass == null) {
	    throw new NullPointerException("codecClass cannot be null");
	}

	// Try to remove the old codec class
	return classes.remove(getCodecBindingType(codecClass)) != null;
    }

    public void unbindAll() {
	// Clear the classes-map
	classes.clear();
    }
}
